Pular para o conteúdo principal

Guia de Verificação — API de Conhecimento RAG + Sistema de Testes

Este documento consolida o guia operacional da API (endpoints, curl, Swagger, Postman, GCP) e o manual técnico do sistema de testes automatizados em um único ponto de referência.


Estatísticas do Sistema de Testes

MétricaValor AtualTarget
Total de Testes96≥ 95
Taxa de Sucesso100% (96/96)100%
Cobertura de Código88.53%≥ 70%
Tempo de Execução~10s< 30s
Testes Unitários~76
Testes de Integração~20

Tecnologias de Teste:

  • Framework: pytest 7.4.3+
  • Cobertura: coverage.py
  • Async: pytest-asyncio
  • Threshold mínimo: 70% (configurado em pytest.ini)

1. Testes Automatizados

1.1 Execução Básica

cd arquitetura_RAG

# Todos os testes
./venv/bin/pytest tests/ -v

# Por tipo
./venv/bin/pytest -m unit -v
./venv/bin/pytest -m integration -v
./venv/bin/pytest -m critical -v

# End-to-End
./venv/bin/pytest tests/integration/test_e2e_knowledge_lifecycle.py -v

# Sem cobertura (mais rápido)
./venv/bin/pytest tests/ -v --no-cov

1.2 Execução Avançada

# Arquivo específico
./venv/bin/pytest tests/unit/test_chunking.py -v

# Teste específico
./venv/bin/pytest tests/unit/test_chunking.py::TestSafeRecursiveSplit::test_table_not_broken -v

# Modo silencioso
./venv/bin/pytest tests/ -q

# Parar no primeiro erro
./venv/bin/pytest tests/ -x

# Reexecutar apenas testes que falharam
./venv/bin/pytest --lf

# Com cobertura e relatório HTML
./venv/bin/pytest tests/ --cov --cov-report=html
xdg-open htmlcov/index.html # Linux

# Relatório de cobertura no terminal
./venv/bin/pytest tests/ --cov --cov-report=term-missing

1.3 Estrutura de Diretórios

arquitetura_RAG/
├── tests/ # Diretório principal de testes
│ ├── conftest.py # Fixtures compartilhadas
│ ├── __init__.py
│ ├── unit/ # Testes unitários
│ │ ├── __init__.py
│ │ ├── test_chunking.py # Chunking de documentos
│ │ ├── test_retrieval_logic.py # Lógica de busca RAG
│ │ ├── test_embeddings.py # Geração de embeddings
│ │ ├── test_llm_metadata.py # Inferência de metadados via LLM
│ │ ├── test_url_fetcher.py # Fetch e parse de URLs
│ │ ├── test_vertex_service.py # Upsert/search/delete no Vertex AI
│ │ └── test_firestore_service.py # CRUD no Firestore
│ └── integration/ # Testes de integração
│ ├── __init__.py
│ ├── test_api_contract.py # Contrato dos endpoints
│ └── test_e2e_knowledge_lifecycle.py # Ciclo completo: ingest→chat→delete
├── pytest.ini # Configuração do pytest
├── requirements-dev.txt # Dependências de teste
├── htmlcov/ # Relatórios de cobertura HTML (gerado)
└── .coverage # Banco de dados de cobertura (gerado)

pytest.ini

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*

addopts =
--verbose
--strict-markers
--color=yes
--tb=short
--cov=app_rag
--cov-fail-under=70

markers =
unit: Testes unitários
integration: Testes de integração
critical: Testes de funcionalidades críticas

1.4 Módulos de Teste Detalhados

test_chunking.py

Arquivo testado: app_rag/services/chunking.py

Funções testadas: smart_parent_split(), smart_recursive_split()

Justificativa: Tabelas tributárias não podem ser quebradas no meio — isso levaria a informações incorretas no RAG.

TesteDescriçãoMarcador
test_parent_split_respects_limitNenhum parent excede 6000 charsunit
test_parent_split_prefers_paragraph_breaksQuebras preferem parágrafos, não meio de frasesunit
test_chunks_respect_size_limitChunks não excedem 800 charsunit
test_chunks_have_overlapOverlap de 200 chars preserva contextounit
test_table_not_brokenTabelas Markdown permanecem íntegrasunit, critical
test_empty_text_handlingStrings vazias não geram exceçõesunit
test_very_short_text_not_chunkedTextos < chunk_size não são divididosunit
pytest tests/unit/test_chunking.py -v
pytest tests/unit/test_chunking.py::TestSafeRecursiveSplit::test_table_not_broken -v

test_retrieval_logic.py

Arquivo testado: app_rag/main.py

Função testada: retrieve_context(query_vector, k=5, filtro_tipo=None)

Justificativa: Núcleo do RAG. Implementa busca vetorial, boost para blogs, threshold e deduplicação.

TesteDescriçãoMarcador
test_blog_boost_applied_correctlyBlogs recebem boost de 15% (0.70 → 0.805)unit, critical
test_threshold_minimum_scoreDocumentos com score < 0.50 são descartadosunit, critical
test_parent_deduplicationMúltiplos chunks do mesmo parent: mantém só o de maior scoreunit, critical
test_filter_by_type_blogFiltro por tipo "blog" funcionaunit
test_filter_by_type_conversaFiltro por tipo "conversa" funcionaunit
test_empty_results_from_vertexResultados vazios sem exceçõesunit
test_top_k_limit_respectedParâmetro k limita número de documentosunit
pytest tests/unit/test_retrieval_logic.py -v

test_embeddings.py

Arquivo testado: app_rag/services/embedding.py

Funções testadas: get_embedding(), get_embeddings_batch()

TesteDescriçãoMarcador
test_newlines_replaced_by_space\n substituídos antes de envio à APIunit
test_returns_1536_dimensionsEmbedding retorna vetor de 1536 dimensõesunit
test_empty_text_handlingTexto vazio é enviado ao cliente e retorna embedding normalmente (contrato determinístico)unit
test_batch_returns_correct_countBatch retorna 1 embedding por textounit
test_batch_replaces_newlinesBatch substitui \n em todos os textosunit
pytest tests/unit/test_embeddings.py -v

test_llm_metadata.py

Arquivo testado: app_rag/services/llm_metadata.py

Funções testadas: infer_blog_metadata(), infer_qa_metadata()

Justificativa: Valida que a inferência de metadados via LLM (gpt-4o-mini) retorna BlogMetadata e QAMetadata com os campos corretos, mockando o cliente OpenAI.

pytest tests/unit/test_llm_metadata.py -v

test_url_fetcher.py

Arquivo testado: app_rag/services/url_fetcher.py

Função testada: fetch_url_content(url) → retorna UrlContent(text, title)

Justificativa: Valida fetch de URL, conversão HTML→Markdown, proteção anti-SSRF (_validate_url bloqueia hosts internos e protocolos não-HTTP) e tratamento de URLs inválidas ou indisponíveis.

pytest tests/unit/test_url_fetcher.py -v

test_vertex_service.py

Arquivo testado: app_rag/services/vertex_service.py

Funções testadas: upsert_datapoints(), remove_datapoints(), search_neighbors()

Justificativa: Valida operações no Vertex AI Vector Search mockando o endpoint, verificando formato de datapoints e tratamento de erros.

pytest tests/unit/test_vertex_service.py -v

test_firestore_service.py

Arquivo testado: app_rag/services/firestore_service.py

Funções testadas: create_document(), get_document(), delete_document(), find_documents_by_base_article_id(), list_documents()

Justificativa: Valida CRUD no Firestore (rag-db-v1, collection knowledge_base) mockando o cliente. Inclui testes de mapeamento dos filtros de tipo:

  • tipo=blogWHERE type == "blog_section"
  • tipo=conversaWHERE type != "blog_section" (inclui todos os não-blog)
pytest tests/unit/test_firestore_service.py -v

test_api_contract.py

Arquivo testado: app_rag/main.py

Endpoints testados: /knowledge/ingest/blog, /knowledge/ingest/qa, /knowledge/delete, GET /knowledge, /chat

Justificativa: Valida o contrato completo da API — campos de request/response, status codes e comportamento dos endpoints.

TesteDescriçãoMarcador
test_chat_endpoint_valid_requestRequest válido retorna fontesintegration, critical
test_response_structureResponse tem campo fontes, sem campo respostaintegration
test_fontes_structureCada fonte tem id, score, conteudo, titulo, tipointegration
test_filtro_tipo_blog_appliedFiltro por tipo "blog" é aplicadointegration
test_no_results_returns_empty_fontesSem resultados retorna {"fontes": []}integration
pytest tests/integration/test_api_contract.py -v

test_e2e_knowledge_lifecycle.py

Arquivo testado: app_rag/main.py (fluxo completo)

Justificativa: Testa o ciclo completo (ingestão → busca → exclusão → verificação de remoção), mockando apenas as APIs externas.

ClasseTesteDescriçãoMarcador
TestKnowledgeLifecycleE2Etest_qa_ingest_then_chat_then_deleteCiclo completo Q&Aintegration, critical
TestKnowledgeLifecycleE2Etest_artigo_ingest_then_chat_then_deleteCiclo completo Artigointegration, critical
TestPerformanceMetricstest_qa_ingest_under_5_secondsQ&A ingestão < 5sintegration
TestPerformanceMetricstest_artigo_ingest_under_30_secondsArtigo ingestão < 30sintegration
TestChatRegressiontest_chat_with_blog_resultChat com resultado blogintegration
TestChatRegressiontest_chat_with_conversa_resultChat com resultado conversaintegration
TestChatRegressiontest_chat_no_results_gracefulChat sem resultadosintegration
pytest tests/integration/test_e2e_knowledge_lifecycle.py -v

1.5 Fixtures e Mocking

Fixtures definidas em tests/conftest.py e injetadas automaticamente pelo pytest.

Fixtures de Mocks

FixtureO que simula
mock_firestore_clientCliente Firestore (sem acesso real ao banco)
mock_vertex_endpointVertex AI Endpoint (upsert, search, remove)
mock_openai_clientCliente OpenAI (embeddings, chat completions)
def test_exemplo(mock_openai_client):
mock_openai_client.embeddings.create.return_value.data[0].embedding = [0.1] * 1536
# ... teste ...

Fixtures de Dados

FixtureRetorna
sample_query_vectorLista de 1536 floats
sample_blog_documentDict com estrutura de documento de blog
sample_conversa_documentDict com estrutura de conversa
sample_vertex_neighborsLista de neighbors mockados
sample_source_docsLista de objetos SourceDoc

Padrão de Mocking com @patch

@patch('main.search_neighbors')
@patch('main.get_embedding')
@pytest.mark.asyncio
async def test_exemplo(self, mock_embed, mock_search):
mock_embed.return_value = [0.1] * 1536
mock_search.return_value = [Mock(id="doc1_chk0", distance=0.85)]
response = await chat(ChatRequest(pergunta="Teste?"))
assert len(response.fontes) == 1

Padrão AAA

def test_exemplo():
# ARRANGE
entrada = "valor de teste"
esperado = "resultado esperado"

# ACT
resultado = funcao(entrada)

# ASSERT
assert resultado == esperado

1.6 Cobertura de Código

# Relatório no terminal
./venv/bin/pytest tests/ --cov --cov-report=term-missing

# Relatório HTML interativo
./venv/bin/pytest tests/ --cov --cov-report=html
xdg-open htmlcov/index.html

Status atual: 88.53% (acima do threshold de 70%).

Interpretação do relatório:

  • Stmts: Total de linhas de código
  • Miss: Linhas não testadas
  • Cover: Porcentagem de cobertura
  • Missing: Números das linhas não testadas

1.7 Resolução de Problemas

ProblemaCausaSolução
pytest: command not foundvenv não ativadosource venv/bin/activate ou usar ./venv/bin/pytest
ModuleNotFoundError: No module named 'pytest'pytest não instalado no venv./venv/bin/pip install -r requirements-dev.txt
FileNotFoundError: pytest.iniExecutando pytest no diretório erradoExecutar a partir de arquitetura_RAG/
ImportError: cannot import name 'X'Caminho incorreto para móduloVerificar sys.path.insert no topo dos arquivos de teste
Coverage.py warning: No data was collectedConfiguração incorretaVerificar pytest.ini e rodar pytest --collect-only
FAILED coverage: 70% not reachedCobertura abaixo do thresholdAdicionar mais testes ou usar --no-cov-fail temporariamente
PermissionError: htmlcov/index.htmlArquivo aberto em outro programarm -rf htmlcov/ && pytest tests/ --cov --cov-report=html

1.8 Boas Práticas

Antes de commitar:

./venv/bin/pytest tests/ -v          # todos os testes
./venv/bin/pytest tests/ --cov # verificar cobertura

Nomenclatura:

  • Arquivos: test_*.py
  • Classes: Test*
  • Funções: test_*
  • Nomes descritivos do comportamento testado (ex: test_blog_boost_applied_correctly)

Marcadores — sempre categorizar:

@pytest.mark.unit
@pytest.mark.critical
def test_funcionalidade_critica():
"""
Descrição do que está sendo testado.
Cenário: entrada → saída esperada.
"""
...

Mocking — nunca acessar APIs externas nos testes:

  • Usar @patch('main.funcao') para serviços (OpenAI, Firestore, Vertex AI)
  • Fixtures em conftest.py para dados reutilizáveis

2. Iniciar a API Localmente

cd arquitetura_RAG/app_rag
../venv/bin/uvicorn main:app --reload --port 8000

3. Mapa de Endpoints

#MetodoRotaDescricao
1POST/knowledge/ingest/blogIngestao de artigo (URL, arquivo ou texto). LLM infere metadados.
2POST/knowledge/ingest/qaIngestao de Q&A (arquivo ou texto). LLM infere metadados.
3POST/knowledge/deleteExclusao unificada (1 ou mais IDs separados por virgula)
4GET/knowledgeListagem com paginacao
5POST/chatBusca RAG — retorna apenas fontes (sem resposta LLM)

Fluxo de ingestao

Ambos os endpoints de ingestao chamam o LLM automaticamente para inferir os metadados (titulo, categoria, tags). Para blogs, o LLM tambem reestrutura o conteudo em markdown limpo.


4. Testes via Terminal (curl)

4.1 Ingerir Blog via URL

curl -s -X POST http://localhost:8000/knowledge/ingest/blog \
-F "url=https://contabilizei.com.br/contabilidade/o-que-e-contabilidade" \
| python3 -m json.tool

Resposta esperada:

{
"status": "success",
"message": "Artigo ingerido com sucesso",
"documento": {
"id": "api_ingest_20260217XXXXXX_Titulo_Inferido_pela_LLM",
"tipo": "artigo",
"titulo": "Titulo extraido pela LLM da pagina",
"categoria": "Contabilidade",
"tags": ["contabilidade", "empresa", "CNPJ"],
"chunks_criados": 5,
"vetores_criados": 5,
"parents_criados": 3,
"resumo": "Resumo gerado pela LLM em 2-3 linhas."
}
}

4.2 Ingerir Blog via Arquivo

cat > /tmp/artigo.md << 'EOF'
MEI: tudo o que voce precisa saber

O Microempreendedor Individual e a forma mais simples de formalizar
um negocio no Brasil. Oferece CNPJ, emissao de nota fiscal e beneficios.

Quem pode ser MEI?
Profissionais que faturam ate R$81 mil por ano e atuam em atividades permitidas.

Quanto custa ser MEI?
O DAS (boleto mensal) custa em torno de R$71 a R$75 por mes em 2026.
EOF

curl -s -X POST http://localhost:8000/knowledge/ingest/blog \
-F "arquivo=@/tmp/artigo.md" \
| python3 -m json.tool

4.3 Ingerir Blog via Texto Raw

curl -s -X POST http://localhost:8000/knowledge/ingest/blog \
-F "conteudo=O Simples Nacional e um regime tributario simplificado para micro e pequenas empresas no Brasil. Ele unifica o pagamento de tributos em uma unica guia, o DAS. As empresas podem optar pelo Simples Nacional se tiverem faturamento ate R\$4,8 milhoes por ano." \
| python3 -m json.tool

4.4 Ingerir Q&A via Texto Raw

curl -s -X POST http://localhost:8000/knowledge/ingest/qa \
-F "conteudo=Pergunta: O que e o DAS do MEI? Resposta: O DAS (Documento de Arrecadacao do Simples Nacional) e o boleto mensal que todo MEI deve pagar para manter seu CNPJ ativo e ter acesso a beneficios previdenciarios. O valor em 2026 e de R\$71 para comercio e R\$75 para servicos." \
| python3 -m json.tool

Resposta esperada:

{
"status": "success",
"message": "Conhecimento Q&A ingerido com sucesso",
"documento": {
"id": "api_ingest_20260217XXXXXX_Titulo_Inferido_pela_LLM",
"tipo": "qa",
"titulo": "O que e o DAS do MEI e quanto custa",
"categoria": "Tributacao",
"tags": ["MEI", "DAS", "boleto"],
"chunks_criados": 1,
"vetores_criados": 1,
"parents_criados": null,
"resumo": null
}
}

ANOTE O ID RETORNADO — voce vai precisar dele para exclusao.

4.5 Ingerir Q&A via Upload de Arquivo

echo "Pergunta: Qual o prazo do DAS? Resposta: O prazo e ate o dia 20 de cada mes. Se cair no fim de semana ou feriado, o prazo antecipa para o ultimo dia util anterior." > /tmp/qa.txt

curl -s -X POST http://localhost:8000/knowledge/ingest/qa \
-F "arquivo=@/tmp/qa.txt" \
| python3 -m json.tool

4.6 Verificar no /chat

# Busca que deve retornar as fontes do conteudo recem-ingerido
curl -s -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"pergunta": "O que e o DAS do MEI?"}' | python3 -m json.tool

# Filtrar apenas por blog
curl -s -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"pergunta": "O que e o Simples Nacional?", "filtro_tipo": "blog"}' | python3 -m json.tool

Resposta esperada:

{
"fontes": [
{
"id": "api_ingest_20260219XXXXXX_titulo_secao",
"score": 0.92,
"conteudo": "Texto do chunk recuperado...",
"titulo": "Titulo do artigo",
"url": "https://contabilizei.com.br/...",
"tipo": "blog"
}
]
}

O campo resposta foi removido — o agente consumidor e responsavel por gerar a resposta final com base nas fontes.

4.7 Listar Conhecimento

# Listar todos
curl -s http://localhost:8000/knowledge | python3 -m json.tool

# Filtrar por tipo
curl -s "http://localhost:8000/knowledge?tipo=conversa" | python3 -m json.tool # Q&A e conversas
curl -s "http://localhost:8000/knowledge?tipo=blog" | python3 -m json.tool # Artigos (alias: blog_section)

# Com paginacao
curl -s "http://localhost:8000/knowledge?limit=10&offset=0" | python3 -m json.tool

Valores do filtro tipo:

  • tipo=blog ou tipo=blog_section — retorna seções de artigos (type == "blog_section")
  • tipo=conversa — retorna Q&A e conversas (type != "blog_section")
  • sem filtro — retorna todos os documentos

4.8 Validações de Segurança

# URL para host interno bloqueado (anti-SSRF) → 422
curl -s -X POST http://localhost:8000/knowledge/ingest/blog \
-F "url=http://169.254.169.254/metadata" | python3 -m json.tool
# {"detail": "URL aponta para recurso interno bloqueado."}

# Protocolo inválido → 422
curl -s -X POST http://localhost:8000/knowledge/ingest/blog \
-F "url=file:///etc/passwd" | python3 -m json.tool
# {"detail": "Protocolo não permitido: 'file'. Use http ou https."}

# Arquivo muito grande (> 100KB) → 422
# Conteúdo muito curto (< 10 chars) → 422
# Extensão não suportada (.csv, .pdf) → 422

4.9 Excluir 1 Documento

curl -s -X POST http://localhost:8000/knowledge/delete \
-H "Content-Type: application/json" \
-d '{"ids": "SEU_DOCUMENT_ID"}' | python3 -m json.tool

4.10 Excluir Multiplos Documentos

curl -s -X POST http://localhost:8000/knowledge/delete \
-H "Content-Type: application/json" \
-d '{"ids": "ID_1, ID_2, ID_3"}' | python3 -m json.tool

Resposta esperada (todos removidos):

{
"status": "success",
"message": "3 de 3 documentos removidos",
"total_solicitados": 3,
"total_removidos": 3,
"resultados": [
{"document_id": "ID_1", "tipo": "conversa", "status": "success"},
{"document_id": "ID_2", "tipo": "blog_section", "status": "success"},
{"document_id": "ID_3", "tipo": "conversa", "status": "success"}
]
}

5. Testes via Swagger UI

Acesse http://localhost:8000/docs

5.1 Ingerir Blog

  1. Clique em POST /knowledge/ingest/blog > "Try it out"
  2. Escolha o modo:
    • URL: preencha apenas url com a URL do artigo
    • Arquivo: no campo arquivo, clique em "Choose File" e selecione .txt ou .md
    • Texto: preencha apenas conteudo com o texto raw
  3. Clique "Execute"
  4. Verifique: status 201, "status": "success", titulo/categoria/tags inferidos

5.2 Ingerir Q&A

  1. Clique em POST /knowledge/ingest/qa > "Try it out"
  2. Escolha o modo:
    • Arquivo: selecione arquivo .txt ou .md
    • Texto: preencha conteudo com o texto do Q&A
  3. Clique "Execute"
  4. Verifique: status 201, "chunks_criados": 1, titulo/categoria/tags inferidos

5.3 Excluir Documentos

  1. Clique em POST /knowledge/delete > "Try it out"
  2. Cole no body:
{"ids": "SEU_ID_AQUI"}

Ou multiplos:

{"ids": "ID_1, ID_2, ID_3"}
  1. Clique "Execute"
  2. Verifique: total_removidos == numero de IDs validos

5.4 Chat

  1. Clique em POST /chat > "Try it out"
  2. Cole no body:
{
"pergunta": "O que e o DAS do MEI?"
}
  1. Clique "Execute"
  2. Verifique: response contem campo fontes com documentos relevantes (sem campo resposta)

6. Testes via Postman

6.1 Configuracao Inicial

  1. Abra o Postman e crie uma nova Collection: "Contabilizei RAG API"
  2. Crie uma variavel de ambiente baseUrl com valor http://localhost:8000

6.2 Ingerir Blog via URL

  • Metodo: POST
  • URL: {{baseUrl}}/knowledge/ingest/blog
  • Body: selecione form-data
KeyValueType
urlhttps://contabilizei.com.br/contabilidade/o-que-e-contabilidadeText

Clique em Send.

6.3 Ingerir Blog via Arquivo

  • Metodo: POST
  • URL: {{baseUrl}}/knowledge/ingest/blog
  • Body: selecione form-data
KeyValueType
arquivoselecione o arquivo .txt ou .mdFile

No campo Type da linha arquivo, mude de Text para File, depois clique em "Select Files".

6.4 Ingerir Blog via Texto Raw

  • Metodo: POST
  • URL: {{baseUrl}}/knowledge/ingest/blog
  • Body: selecione form-data
KeyValueType
conteudoO Simples Nacional e um regime tributario para PMEs...Text

6.5 Ingerir Q&A via Texto Raw

  • Metodo: POST
  • URL: {{baseUrl}}/knowledge/ingest/qa
  • Body: selecione form-data
KeyValueType
conteudoPergunta: O que e o DAS? Resposta: O DAS e o boleto mensal do MEI.Text

6.6 Ingerir Q&A via Arquivo

  • Metodo: POST
  • URL: {{baseUrl}}/knowledge/ingest/qa
  • Body: selecione form-data
KeyValueType
arquivoselecione o arquivo .txt ou .mdFile

6.7 Excluir Documentos

  • Metodo: POST
  • URL: {{baseUrl}}/knowledge/delete
  • Body: selecione raw + JSON
{
"ids": "api_ingest_20260217XXXXXX_titulo"
}

Para multiplos:

{
"ids": "ID_1, ID_2, ID_3"
}

6.8 Listar Conhecimento

  • Metodo: GET
  • URL: {{baseUrl}}/knowledge
  • Params (opcional):
KeyValue
tipoconversa
limit10
offset0

6.9 Chat

  • Metodo: POST
  • URL: {{baseUrl}}/chat
  • Body: selecione raw + JSON
{
"pergunta": "O que e o DAS do MEI?"
}

6.10 Dicas no Postman

  • Salvar IDs: no painel "Tests" de cada requisicao de ingestao, cole o script para salvar o ID automaticamente:
const json = pm.response.json();
pm.environment.set("last_doc_id", json.documento.id);
  • Depois use {{last_doc_id}} nos campos de exclusao.

7. Verificacao Direta no Google Cloud

7.1 Firestore

Via Console Web:

  1. Acesse https://console.cloud.google.com/firestore
  2. Projeto: ctbz-ia-assessoria-poc | Banco: rag-db-v1
  3. Collection: knowledge_base
  4. Verifique campos: type, source, base_article_id (artigos), metadata.categoria, metadata.tags

Via Python:

from google.cloud import firestore
db = firestore.Client(project="ctbz-ia-assessoria-poc", database="rag-db-v1")

# Listar ingestoes via API
docs = db.collection("knowledge_base") \
.where("source", "==", "api_ingest_v1") \
.limit(20).stream()

for doc in docs:
d = doc.to_dict()
print(f"{doc.id} | {d.get('type')} | {d.get('title','')[:60]}")
print(f" categoria: {d.get('metadata', {}).get('categoria')}")
print(f" tags: {d.get('metadata', {}).get('tags')}")

Via gcloud:

gcloud ai indexes describe 2352965878556917760 \
--region=us-central1 \
--project=ctbz-ia-assessoria-poc

8. Checklist de Verificacao Final

Testes Automatizados

  • ./venv/bin/pytest tests/ -v - Todos os testes passam
  • Cobertura >= 70%

Blog — URL

  • POST /knowledge/ingest/blog com url retorna 201
  • LLM infere titulo, categoria, tags e resumo automaticamente
  • parents_criados >= 1, chunks_criados >= 1
  • /chat retorna o conteudo quando pergunta relacionada

Blog — Arquivo / Texto

  • POST /knowledge/ingest/blog com arquivo (.md) retorna 201
  • POST /knowledge/ingest/blog com conteudo retorna 201
  • LLM reestrutura o conteudo em markdown
  • Extensao invalida (.csv) retorna 422

Q&A — Arquivo / Texto

  • POST /knowledge/ingest/qa com conteudo retorna 201
  • POST /knowledge/ingest/qa com arquivo (.txt) retorna 201
  • LLM infere titulo, categoria e tags
  • chunks_criados == 1, vetores_criados == 1
  • /chat retorna o conteudo quando pergunta relacionada

Exclusao

  • POST /knowledge/delete com 1 ID remove corretamente
  • POST /knowledge/delete com N IDs (comma-separated) remove todos
  • ID inexistente retorna "status": "partial" com mensagem de erro
  • Exclusao de artigo remove todos os parents + vetores
  • /chat NAO retorna conteudo excluido

Listagem e Chat

  • GET /knowledge retorna lista paginada
  • GET /knowledge?tipo=conversa filtra corretamente
  • /chat retorna {"fontes": [...]} com documentos relevantes
  • /chat sem resultados retorna {"fontes": []}

9. Logs de Observabilidade

# Blog via URL
INFO: Ingestao blog URL: url='https://...'
INFO: Inferindo metadados de blog via LLM (gpt-4o-mini), content_len=5234
INFO: Metadados blog inferidos: titulo='...' categoria='...' tags=[...]
INFO: Ingestao blog concluida: id=... titulo='...' chunks=5 duration_ms=3210

# Q&A via texto
INFO: Ingestao Q&A texto
INFO: Inferindo metadados de Q&A via LLM (gpt-4o-mini), content_len=342
INFO: Metadados Q&A inferidos: titulo='...' categoria='...' tags=[...]
INFO: Ingestao Q&A concluida: id=... titulo='...' duration_ms=1450

# Exclusao
INFO: Exclusao: 2 documento(s) solicitado(s)
INFO: Exclusao concluida: 2/2 removidos duration_ms=890

Metricas de performance esperadas:

  • Ingestao Q&A: < 5.000ms (inclui chamada LLM)
  • Ingestao blog (10 secoes): < 30.000ms (inclui chamada LLM)
  • Ingestao via URL: < 60.000ms (inclui download + LLM)
  • Exclusao unitaria: < 10.000ms
  • Exclusao em lote (10 IDs): < 30.000ms
  • Chat (busca RAG): < 2.000ms (sem chamada LLM)

10. Integracao com o Agente do Pedro (Cloud Run)

URL Base da API (producao)

https://rag-api-55837972640.us-central1.run.app

Nao use a URL do Swagger (/docs) — use a URL base acima.

Autenticacao (service-to-service GCP)

O servico esta privado. O agente do Pedro (agente-contabil) ja tem permissao roles/run.invoker. A autenticacao e feita via token OIDC da service account do Cloud Run — sem configuracao adicional.

import google.auth.transport.requests
from google.oauth2 import id_token
import requests

RAG_URL = "https://rag-api-55837972640.us-central1.run.app"

def buscar_fontes(pergunta: str, filtro_tipo: str = None) -> list[dict]:
auth_req = google.auth.transport.requests.Request()
token = id_token.fetch_id_token(auth_req, RAG_URL)

payload = {"pergunta": pergunta}
if filtro_tipo:
payload["filtro_tipo"] = filtro_tipo # "blog" ou "conversa"

response = requests.post(
f"{RAG_URL}/chat",
json=payload,
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
response.raise_for_status()
return response.json()["fontes"]

Campos de cada fonte retornada

CampoTipoDescricao
idstrID do documento no Firestore
scorefloatRelevancia (0.50 a ~1.15)
conteudostrTexto do chunk recuperado
titulostrTitulo do artigo ou arquivo
urlstr ou nullURL original (blogs)
tipostr"blog" ou "conversa"